import { PropType, defineComponent, onMounted, ref, watchEffect } from "vue";
import qrcodegen from "./qrcodegen";

export interface QRCodeProps {
    level?: string
    value: string
    size?: number
    style?: any
    color?: string
    bgColor?: string
    includeMargin?: boolean
    marginSize?: number
    icon?: string
    imageSettings?: ImageSettings
    title?: string,
}

const QRCodeBaseProps = {
    level: {type: String as PropType<QRCodeProps['level']>, default: undefined},
    bgColor: {type: String as PropType<QRCodeProps['bgColor']>, default: undefined},
    color: {type: String as PropType<QRCodeProps['color']>, default: undefined},
    value: {type: String as PropType<QRCodeProps['value']>, required: true},
    style: {type: Object as PropType<QRCodeProps['style']>},
    size: {type: Number as PropType<QRCodeProps['size']>, default: 200},
    includeMargin: {type: Boolean as PropType<QRCodeProps['includeMargin']>},
    marginSize: {type: Number as PropType<QRCodeProps['marginSize']>},
    icon: {type: String as PropType<QRCodeProps['icon']>},
    imageSettings: {type: Object as PropType<QRCodeProps['imageSettings']>},
    title: {type: String as PropType<QRCodeProps['title']>},
};

const ERROR_LEVEL_MAP: { [index: string]: qrcodegen.QrCode.Ecc } = {
    L: qrcodegen.QrCode.Ecc.LOW,
    M: qrcodegen.QrCode.Ecc.MEDIUM,
    Q: qrcodegen.QrCode.Ecc.QUARTILE,
    H: qrcodegen.QrCode.Ecc.HIGH,
};

type ImageSettings = {
    src: string;
    height?: number;
    width?: number;
    excavate: boolean;
    x?: number;
    y?: number;
};

const DEFAULT_SIZE = 128;
const DEFAULT_LEVEL = 'L';
const DEFAULT_BGCOLOR = '#FFFFFF';
const DEFAULT_FGCOLOR = '#000000';
const DEFAULT_INCLUDEMARGIN = false;
const DEFAULT_IMG_SCALE = 0.25;

const SPEC_MARGIN_SIZE = 4;
const DEFAULT_MARGIN_SIZE = 0;

type Modules = ReturnType<qrcodegen.QrCode['getModules']>;
type Excavation = { x: number; y: number; w: number; h: number };

function generatePath(modules: Modules, margin: number = 0): string {
    const ops: Array<string> = [];
    modules.forEach(function (row, y) {
        let start: number | null = null;
        row.forEach(function (cell, x) {
            if (!cell && start !== null) {
                // M0 0h7v1H0z injects the space with the move and drops the comma,
                // saving a char per operation
                ops.push(
                    `M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`
                );
                start = null;
                return;
            }

            // end of row, clean up or skip
            if (x === row.length - 1) {
                if (!cell) {
                    // We would have closed the op above already so this can only mean
                    // 2+ light modules in a row.
                    return;
                }
                if (start === null) {
                    // Just a single dark module.
                    ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`);
                } else {
                    // Otherwise finish the current line.
                    ops.push(
                        `M${start + margin},${y + margin} h${x + 1 - start}v1H${start + margin
                        }z`
                    );
                }
                return;
            }

            if (cell && start === null) {
                start = x;
            }
        });
    });
    return ops.join('');
}

function getMarginSize(includeMargin: boolean, marginSize?: number): number {
    if (marginSize != null) {
        return Math.floor(marginSize);
    }
    return includeMargin ? SPEC_MARGIN_SIZE : DEFAULT_MARGIN_SIZE;
}

function getImageSettings(
    cells: Modules,
    size: number,
    margin: number,
    imageSettings?: ImageSettings
): null | {
    x: number;
    y: number;
    h: number;
    w: number;
    excavation: Excavation | null;
} {
    if (imageSettings == null) {
        return null;
    }
    const numCells = cells.length + margin * 2;
    const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE);
    const scale = numCells / size;
    const w = (imageSettings.width || defaultSize) * scale;
    const h = (imageSettings.height || defaultSize) * scale;
    const x =
        imageSettings.x == null
            ? cells.length / 2 - w / 2
            : imageSettings.x * scale;
    const y =
        imageSettings.y == null
            ? cells.length / 2 - h / 2
            : imageSettings.y * scale;

    let excavation = null;
    if (imageSettings.excavate) {
        let floorX = Math.floor(x);
        let floorY = Math.floor(y);
        let ceilW = Math.ceil(w + x - floorX);
        let ceilH = Math.ceil(h + y - floorY);
        excavation = { x: floorX, y: floorY, w: ceilW, h: ceilH };
    }

    return { x, y, h, w, excavation };
}

function excavateModules(modules: Modules, excavation: Excavation): Modules {
    return modules.slice().map((row, y) => {
        if (y < excavation.y || y >= excavation.y + excavation.h) {
            return row;
        }
        return row.map((cell, x) => {
            if (x < excavation.x || x >= excavation.x + excavation.w) {
                return cell;
            }
            return false;
        });
    });
}

const SUPPORTS_PATH2D = (function () {
    try {
        new Path2D().addPath(new Path2D());
    } catch (e) {
        return false;
    }
    return true;
})();


export const QRCodeCanvas = defineComponent({
    name: "QRCodeCanvas",
    props: QRCodeBaseProps,
    setup(props, {expose}) {
        let {
            value,
            size = DEFAULT_SIZE,
            level = DEFAULT_LEVEL,
            bgColor = DEFAULT_BGCOLOR,
            color = DEFAULT_FGCOLOR,
            includeMargin = DEFAULT_INCLUDEMARGIN,
            marginSize,
            style,
            icon,
            imageSettings,
            ...otherProps
        } = props;
        imageSettings = imageSettings ?? icon ? {
            excavate: true,
            src: undefined
        } : undefined

        const imgSrc = icon;
        let _canvas: any;
        let _image: any;

        expose({
            download: () => {
                const url = _canvas.toDataURL('image/png')
                if ('download' in document.createElement('a')) { // 非IE下载
                    const elink = document.createElement('a');
                    elink.download = '';
                    elink.style.display = 'none';
                    elink.href = url;
                    document.body.appendChild(elink);
                    elink.click();
                    URL.revokeObjectURL(elink.href); // 释放URL 对象
                    document.body.removeChild(elink);
                }
            }
        })

        // We're just using this state to trigger rerenders when images load. We
        // Don't actually read the value anywhere. A smarter use of useEffect would
        // depend on this value.
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const isImgLoaded = ref(false);

        onMounted(() => {
            watchEffect(() => {
                const val = props.value;
                // Always update the canvas. It's cheap enough and we want to be correct
                // with the current state.
                if (_canvas) {
                    const ctx = _canvas.getContext('2d');
                    if (!ctx) {
                        return;
                    }
        
                    let cells = qrcodegen.QrCode.encodeText(
                        val,
                        ERROR_LEVEL_MAP[level]
                    ).getModules();
        
                    const margin = getMarginSize(includeMargin, marginSize);
                    const numCells = cells.length + margin * 2;
                    ctx.clearRect(0, 0, numCells, numCells)
                    const calculatedImageSettings = getImageSettings(
                        cells,
                        size,
                        margin,
                        imageSettings
                    );
        
                    const image = _image;
                    const haveImageToRender = isImgLoaded.value &&
                        calculatedImageSettings != null &&
                        image !== null &&
                        image.complete &&
                        image.naturalHeight !== 0 &&
                        image.naturalWidth !== 0;
        
                    if (haveImageToRender) {
                        if (calculatedImageSettings.excavation != null) {
                            cells = excavateModules(cells, calculatedImageSettings.excavation);
                        }
                    }
        
                    // We're going to scale this so that the number of drawable units
                    // matches the number of cells. This avoids rounding issues, but does
                    // result in some potentially unwanted single pixel issues between
                    // blocks, only in environments that don't support Path2D.
                    const pixelRatio = window.devicePixelRatio || 1;
                    _canvas.height = _canvas.width = size * pixelRatio;
                    const scale = (size / numCells) * pixelRatio;
                    ctx.scale(scale, scale);
        
                    // Draw solid background, only paint dark modules.
                    ctx.fillStyle = bgColor;
                    ctx.fillRect(0, 0, numCells, numCells);
        
                    ctx.fillStyle = color;
                    if (SUPPORTS_PATH2D) {
                        // $FlowFixMe: Path2D c'tor doesn't support args yet.
                        ctx.fill(new Path2D(generatePath(cells, margin)));
                    } else {
                        cells.forEach(function (row, rdx) {
                            row.forEach(function (cell, cdx) {
                                if (cell) {
                                    ctx.fillRect(cdx + margin, rdx + margin, 1, 1);
                                }
                            });
                        });
                    }
        
                    if (haveImageToRender) {
                        ctx.drawImage(
                            image,
                            calculatedImageSettings.x + margin,
                            calculatedImageSettings.y + margin,
                            calculatedImageSettings.w,
                            calculatedImageSettings.h
                        );
                    }
                }
            });
        })

        // Ensure we mark image loaded as false here so we trigger updating the
        // canvas in our other effect.
        watchEffect(() => {
            imgSrc;
            isImgLoaded.value = false;
        });

        const canvasStyle = { height: size + 'px', width: size + 'px', ...style };

        return () => (
            <>
                <canvas
                    style={canvasStyle}
                    height={size}
                    width={size}
                    ref={(el) => _canvas = el}
                    {...otherProps}
                />
                {
                    imgSrc != null ?
                        <img
                            src={imgSrc}
                            style={{ display: 'none' }}
                            onLoad={() => {
                                isImgLoaded.value = true;
                            }}
                            ref={(el) => _image = el}
                        />
                        : null
                }
            </>
        );
    },
});
    

export default defineComponent({
    name: 'QRCode',
    props: QRCodeBaseProps,
    setup (props, {expose}) {
        let canvas: any;
        const download = () => {
            canvas.download();
        }
        expose({
            download
        })
        return () => <div class='cm-qrcode' style={{"background-color": props.bgColor || DEFAULT_BGCOLOR}}>
            {
                <QRCodeCanvas {...props} ref={(el) => canvas = el}/>
            }
        </div>;
    }
})